package weeny.pre.util;

import static java.awt.BasicStroke.CAP_ROUND;
import static java.awt.BasicStroke.JOIN_ROUND;
import static prefuse.visual.VisualItem.FILLCOLOR;
import static prefuse.visual.VisualItem.SCHEMA;
import static prefuse.visual.VisualItem.SIZE;
import static prefuse.visual.VisualItem.STROKECOLOR;
import static prefuse.visual.VisualItem.X;
import static prefuse.visual.VisualItem.Y;

import java.awt.BasicStroke;
import java.awt.geom.Point2D;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import prefuse.Visualization;
import prefuse.data.Edge;
import prefuse.data.Graph;
import prefuse.data.Node;
import prefuse.data.Schema;
import prefuse.data.Table;
import prefuse.data.Tuple;
import prefuse.data.expression.Expression;
import prefuse.data.expression.IfExpression;
import prefuse.data.expression.ObjectLiteral;
import prefuse.data.tuple.TupleSet;
import prefuse.render.Renderer;
import prefuse.util.DataLib;
import prefuse.util.PredicateChain;
import prefuse.util.PrefuseLib;
import prefuse.util.StrokeLib;
import prefuse.visual.EdgeItem;
import prefuse.visual.VisualGraph;
import prefuse.visual.VisualItem;
import prefuse.visual.VisualTable;
import prefuse.visual.expression.GroupExpression;
import prefuse.visual.expression.InGroupPredicate;
import weeny.util.logs.Debug;

/**
 * Some helper functions do dealing with prefuse tables
 * 
 * @author weeny
 */

public class Pre {

	//check or get or create something
	public static BasicStroke roundStroke(float width){
		return StrokeLib.getStroke(width, CAP_ROUND, JOIN_ROUND);
	}
	public static double[] minMaxOf(TupleSet set, String field){
		double[] result = new double[2];
		result[0] = DataLib.min(set, field).getDouble(field);
		result[1] = DataLib.max(set, field).getDouble(field);
		return result;
	}
	public static VisualItem src(VisualItem edge){
		return ((EdgeItem) edge).getSourceItem();
	}
	public static VisualItem tar(VisualItem edge){
		return ((EdgeItem) edge).getTargetItem();
	}
	public static Edge edge(Node node1, Node node2){
		if (node1.getGraph() != node2.getGraph()) {
			throw new IllegalArgumentException("Not in the same graph");
		}
		Graph graph = node1.getGraph();
		Edge edge = graph.getEdge(node1, node2);
		if (edge == null) {
			edge = graph.getEdge(node2, node1);
		}
		return edge;
	}

	/**
	 * get the group name from a {@link InGroupPredicate}
	 * 
	 * @return null if fails
	 */
	public static String groupInPredicate(Expression exp){
		if (!(exp instanceof InGroupPredicate)) {
			Debug.prob("Not a InGroupPredicate!, so return null");
			return null;
		}
		GroupExpression pred = (GroupExpression) exp;
		try {
			Field field = GroupExpression.class.getDeclaredField("m_group");
			field.setAccessible(true);
			Expression literal = (Expression) field.get(pred);
			if (!(literal instanceof ObjectLiteral)) {
				Debug.prob("String Expression instead of String Object, stop");
				return null;
			}
			return ((ObjectLiteral)literal).get(null).toString();
		} catch (Exception e) {
			e.printStackTrace();
		}
		Debug.prob("After the exception, return null");
		return null;
	}
	
	/**
	 * We suppose *ALL* the Objects in the chain are Renderers and collect them
	 * as a list. So when then passed-in parameter does not look like this, you
	 * will get a {@link ClassCastException} exception.
	 */
	public static List<Renderer> getRenders(PredicateChain chain){
		List<Renderer> result = new ArrayList<Renderer>();
		Expression head = chain.getExpression();
		while (head != null && head instanceof IfExpression){
			IfExpression ifExp = (IfExpression) head;
			result.add((Renderer) ifExp.getThenExpression().get(null));
			head = ifExp.getElseExpression();
		}
		return result;
	}

	/**
	 * We suppose all things in the chain is InGroupPredicates and Renderers.
	 * So we collect all the group names that are mapped to the passed-in ren.
	 * The ren can be null, which means collectiong all group names no matter
	 * what!
	 */
	public static List<String> getFocusGroups(PredicateChain pc, Renderer ren){
		List<String> result = new ArrayList<String>();
		Expression head = pc.getExpression();
		while (head != null && head instanceof IfExpression){
			IfExpression ifExp = (IfExpression) head;
			Renderer tempRen = (Renderer) ifExp.getThenExpression().get(null);
			if (ren == null || ren == tempRen){
				Expression pred = ifExp.getTestPredicate();
				if (pred instanceof InGroupPredicate) {
					result.add(groupInPredicate(pred));
				}
				else{
					Debug.prob("It is not an InGroupPredicate, so ignored!");
				}
			}
			head = ifExp.getElseExpression();
		}
		return result;
	}
	
	public static boolean isNodeInEdge(Node node, Edge edge){
		if (edge.getGraph() != node.getGraph()) {
			throw new IllegalArgumentException("Not in the same graph");
		}
		if (edge.getSourceNode() == node || edge.getTargetNode() == node) {
			return true;
		}
		return false;
	}
	public static Node sharedNode(Edge e1, Edge e2){
		if (e1.getGraph() != e2.getGraph()) {
			throw new IllegalArgumentException("Not in the same graph");
		}
		if (e1.getSourceNode() == e2.getSourceNode() || 
			e1.getSourceNode() == e2.getTargetNode()) {
			return e1.getSourceNode();
		}
		if (e1.getTargetNode() == e2.getSourceNode() || 
			e1.getTargetNode() == e2.getTargetNode()) {
			return e1.getTargetNode();
		}
		return null;
	}

	
	//set or update something
	public static void setSize(VisualItem vi, double size){
		PrefuseLib.updateDouble(vi, SIZE, size);
	}
	public static void setBothCol(Object vi, int col){
		Pre.setFilCol(vi, col);
		Pre.setStkCol(vi, col);
	}
	public static void setStkCol(Object vi, int col){
		if(vi instanceof VisualItem) {
			PrefuseLib.updateInt((VisualItem) vi, STROKECOLOR, col);
		}
		else {
			((Tuple)vi).setInt(STROKECOLOR, col);
		}
	}
	public static void setFilCol(Object vi, int col){
		if(vi instanceof VisualItem) {
			PrefuseLib.updateInt((VisualItem) vi, FILLCOLOR, col);
		}
		else {
			((Tuple)vi).setInt(FILLCOLOR, col);
		}
	}
	public static void setX(Object obj, double x){
		if (obj instanceof VisualItem) {
			PrefuseLib.setX((VisualItem) obj, null, x);
		}
		else if (obj instanceof Tuple){
			Tuple tuple = (Tuple) obj;
			tuple.setDouble(X, x);
		}
		else {
			Point2D point = (Point2D) obj;
			point.setLocation(x, point.getY());
		}
	}
	public static void setY(Object obj, double y){
		if (obj instanceof VisualItem) {
			PrefuseLib.setY((VisualItem) obj, null, y);
		}
		else if (obj instanceof Tuple){
			Tuple tuple = (Tuple) obj;
			tuple.setDouble(Y, y);
		}
		else {
			Point2D point = (Point2D) obj;
			point.setLocation(point.getX(), y);
		}
	}
	public static void updateFloat(Tuple tuple, String field, float value) {
		tuple.setFloat(PrefuseLib.getStartField(field), tuple.getFloat(field));
		tuple.setFloat(PrefuseLib.getEndField(field), value);
		tuple.setFloat(field, value);
	}
	public static void updateDouble(Tuple tuple, String field, double value) {
		tuple.setDouble(PrefuseLib.getStartField(field), tuple.getDouble(field));
		tuple.setDouble(PrefuseLib.getEndField(field), value);
		tuple.setDouble(field, value);
	}
	
	public static void shift(Object vi, double dx, double dy){
		Pre.setPos(vi, Geom.X(vi)+dx, Geom.Y(vi)+dy);
	}
	public static void setPos(Object vi, double x, double y){
		setX(vi, x);
		setY(vi, y);
	}
	public static void setPos(Object vi, Object pos){
		setPos(vi, Geom.X(pos), Geom.Y(pos));
	}
	public static void stabilize(Iterator it, String field){
		String sField = PrefuseLib.getStartField(field);
		String eField = PrefuseLib.getEndField(field);
		while (it.hasNext()){
			Tuple t = (Tuple) it.next();
			try {
				//sometimes, it may not have the start/end fields
				t.set(sField, t.get(field));
				t.set(eField, t.get(field));
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	public static void stabilize(TupleSet set, String field){
		stabilize(set.tuples(), field);
	}
	
	
	////////////////////////////////////////////////////////////////////////////
	// Functions to help add data tuple set to visualization, because some    //
	// visual schema columns may exist in the data tuple set, so if we just   //
	// call vis.addSomething, the visualization will create all visual columns//
	// in the inherited table and hide all the columns with same names in the //
	// data tuple set, so we can not use those columns in the data tuple set, //
	// which is not what we want. so we need to either:                       //
	// 1) use addToVis function to generate the complementary column schema   //
	//    and use them, so we will not add duplicated columns in the visual   //
	//    tuple set, or                                                       //
	// 2) call cleanDuplicatedColumn to remove all the duplicated columns in  //
	//    the visual tuple set                                                //
	////////////////////////////////////////////////////////////////////////////
	
	public static VisualGraph addToVis(Visualization vis, String grp, Graph g){
		Schema nSchema = Pre.unique(SCHEMA, g.getNodeTable());
		Schema eSchema = Pre.unique(SCHEMA, g.getEdgeTable());
		Debug.log(eSchema);
		return vis.addGraph(grp, g, null, nSchema, eSchema);
	}
	public static VisualTable addToVis(Visualization vis, String grp, Table t){
		Schema schema = Pre.unique(SCHEMA, t);
		return vis.addTable(grp, t, schema);
	}
	/**
	 * Give all the columns existing in big but not in small.
	 */
	public static Schema unique(Schema big, Table small){
		Schema schema = new Schema();
		for(int i = 0; i<big.getColumnCount(); i++){
			String field = big.getColumnName(i);
			Class type = small.getColumnType(field);
			Object dft = big.getDefault(field);
			if (type == null) {//cannot find in the small schema
				schema.addColumn(field, big.getColumnType(field), dft);
			}
			else if (!big.getColumnType(field).isAssignableFrom(type)) {
				//the types are conflicted
				throw new IllegalStateException(
						"Type conflict! in field ["+ field+"] : find "+type);
			}
			else if (dft != null && small.getDefault(field) == null){
				Debug.prob("Default value conflict! in field ["+ field+"]");
			}
		}
		return schema;
	}
	/**
	 * convenience function of {@link #cleanDuplicatedField(VisualTable, Set)}
	 * for processing VisualGraph
	 */
	public static void cleanDuplicatedField(VisualGraph vGraph){
		Pre.cleanDuplicatedField((VisualTable) vGraph.getNodeTable());
		Pre.cleanDuplicatedField((VisualTable) vGraph.getEdgeTable());
	}
	/**
	 * convenience function of {@link #cleanDuplicatedField(VisualTable, Set)}
	 * for processing VisualTable (having nothing to ignore)
	 */
	public static void cleanDuplicatedField(VisualTable vTable){
		cleanDuplicatedField(vTable, null);
	}
	/**
	 * VisualTable has its parent table. So if they both have a column with the
	 * same name, the child one will hide the parent one. This function will
	 * check every column in the child table and remove it if the same column
	 * is also found in the parent table.
	 * 
	 * @param vTable the VisualTable to be processed
	 * @param ignored we ignore all columns in this set, which means that we do
	 * not check the columns whose names are found in this set.
	 */
	public static void cleanDuplicatedField(VisualTable vTable, Set ignored){
		Table table = vTable.getParentTable();
		Schema removedSchema = new Schema();
		Set<String> columnNames = new HashSet<String>();
		for(int i = 0; i<vTable.getLocalColumnCount(); i++){
			columnNames.add(vTable.getColumnName(i));
		}
		for (String column : columnNames) {
			if (table.getColumn(column)==null){
				continue;
			}
			Class vType = vTable.getColumnType(column);
			if(vType.isAssignableFrom(table.getColumnType(column))) {
				try {
					if (ignored == null || !ignored.contains(column)) {
						vTable.removeColumn(column);
						removedSchema.addColumn(column, vType);
					}
				} catch (RuntimeException e) {
					Debug.prob("CANNOT remove "+column);
				}
			}
		}
		Debug.info("Columns removed: "+removedSchema);	
	}
}
